using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Linq;
using NUnit.Framework;
using seanfoy.paging;

[TestFixture]
public class Tests {
    [TestFixtureSetUp]
    public void setup() {
        Persistence.rebuildDB();
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                Assert.That(s.Linq<Bottle>().Count(), Is.GreaterThan(pageSize));
                Assert.That(s.Linq<Beer>().Count(), Is.GreaterThan(pageSize));
            }
        }
    }

    [Test]
    public void empty() {
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var max =
                    Queryable.Max(
                        s.Linq<Bottle>(),
                        i => i.Serial);
                var source =
                    DetachedCriteria.For(
                        typeof(Bottle));
                Assert.IsFalse(
                    Enumerable.Any(
                        Paging.Page(
                            s,
                            source,
                            new [] {"Serial"},
                            new Bottle { Serial = max + 1 },
                            pageSize)));
            }
        }
    }

    [Test]
    public void singleton() {
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var max =
                    Queryable.Max(
                        s.Linq<Bottle>(),
                        i => i.Serial);
                var source =
                    DetachedCriteria.For(
                        typeof(Bottle)).
                    AddOrder(Order.Asc("Serial"));
                Assert.AreEqual(
                    1,
                    Enumerable.Count(
                        Paging.Page(
                            s,
                            source,
                            new [] {"Serial"},
                            new Bottle { Serial = max },
                            pageSize)));
            }
        }
    }

    [Test]
    public void naturals() {
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var Q =
                    Enumerable.Select(
                        Enumerable.Range(0, 2),
                        x => DetachedCriteria.For<Bottle>().AddOrder(Order.Asc("Serial"))).ToArray();
                Assert.AreEqual(
                    Q[0].GetExecutableCriteria(s).SetMaxResults(pageSize).List<Bottle>(),
                    Paging.Page<Bottle>(s, Q[1], new [] {"Serial"}, null, pageSize));
            }
        }
    }

    [Test]
    public void beers() {
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var O = new [] {"ABV", "BrewDate", "Variety"};
                // ICriterion instances are mutable and don't
                // expose a copy constructor or clone method,
                // so we must make two criteria, just alike.
                var Q =
                    Enumerable.Select(
                        Enumerable.Range(0, 2),
                        x => DetachedCriteria.For<Beer>()).ToArray();
                foreach (var q in Q) {
                    Paging.AddOrder(q, O);
                }
                Assert.AreEqual(
                    Q[0].GetExecutableCriteria(s).SetMaxResults(pageSize).List<Beer>(),
                    Paging.Page<Beer>(s, Q[1], O, null, pageSize));
            }
        }
    }

    private delegate List<Object> FetchPage(ISession s, DetachedCriteria q, IEnumerable<String> O, Object start, int pageSize);
    private List<Object> fetchPageByValueEquality(ISession s, DetachedCriteria q, IEnumerable<String> O, Object start, int pageSize) {
        return Paging.Page(s, q, O, start, pageSize).ToList<Object>();
    }
    private List<Object> fetchPageByOffset(ISession s, DetachedCriteria q, IEnumerable<String> O, Object memento, int pageSize) {
        var start = (int)(memento ?? 0);
        var result = new List<Object>();
        result.AddRange(
            Enumerable.OfType<Object>(
                q.GetExecutableCriteria(s).SetMaxResults(pageSize).SetFirstResult(start).List()));
        result.Add(start + pageSize - 1);
        return result;
    }

    [Test]
    public void insertionSansDuplicates() {
        insertion(fetchPageByValueEquality);
    }

    [Test]
    [ExpectedException(typeof(DuplicateDetectedException))]
    public void insertionAnomalies() {
        insertion(fetchPageByOffset);
    }

    private class AnomalyDetectedException : Exception {}
    private class DuplicateDetectedException : AnomalyDetectedException {}
    private void insertion(FetchPage fetchPage) {
        var q = DetachedCriteria.For<Beer>();
        var O = new [] {"ABV", "BrewDate", "Variety"};
        Paging.AddOrder(q, O);
        var seenit = new HashSet<Beer>();
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var page =
                    fetchPage(
                        s,
                        q,
                        O,
                        null,
                        pageSize + 1);
                foreach (Beer i in Enumerable.TakeWhile(page, (elt, i) => i < pageSize)) {
                    if (seenit.Contains(i)) throw new DuplicateDetectedException();
                    seenit.Add(i);
                }
                var first = (Beer)page[0];
                var nouveau =
                    new Beer {
                        Variety = "not" + first.Variety,
                        BrewDate = first.BrewDate,
                        ABV = first.ABV / 2};
                s.Save(nouveau);
                s.Flush();
                page =
                    fetchPage(s, q, O, Enumerable.Last(page), pageSize);
                foreach (Beer i in page) {
                    if (seenit.Contains(i)) throw new DuplicateDetectedException();
                    seenit.Add(i);
                }
            }
        }
    }

    [Test]
    public void deletionSansSkips() {
        deletion(fetchPageByValueEquality);
    }

    [Test]
    [ExpectedException(typeof(AnomalyDetectedException))]
    public void deletionAnomalies() {
        deletion(fetchPageByOffset);
    }

    private void deletion(FetchPage fetchPage) {
        var q = DetachedCriteria.For<Beer>();
        var O = new [] {"ABV", "BrewDate", "Variety"};
        Paging.AddOrder(q, O);
        var expected = new HashSet<Object>();
        var actual = new HashSet<Object>();
        using (var factory = Persistence.makeSessionFactory()) {
            using (var s = factory.OpenSession()) {
                var page = fetchPage(s, q, O, null, pageSize);
                foreach (var i in page) expected.Add(i);
                foreach (var i in page) actual.Add(i);
                foreach (var i in fetchPage(s, q, O, Enumerable.Last(page), pageSize)) {
                    expected.Add(i);
                }
                s.Delete(page[0]);
                s.Flush();
                foreach (var i in fetchPage(s, q, O, Enumerable.Last(page), pageSize)) {
                    actual.Add(i);
                }

                actual.SymmetricExceptWith(expected);
                if (0 != actual.Count) throw new AnomalyDetectedException();
            }
        }
    }

    const int pageSize = 10;
}
